
import winreg
import subprocess
import os
import re
import ctypes
import sys
import json
import time
import shutil
from tkinter import *
from tkinter import ttk, messagebox, scrolledtext

class ProgramManager:
    @staticmethod
    def is_admin():
        try:
            return ctypes.windll.shell32.IsUserAnAdmin()
        except:
            return False

    @staticmethod
    def get_installed_programs():
        programs = []
        reg_paths = [
            (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"),
            (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"),
            (winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Uninstall"),
        ]
        
        for key_type, reg_path in reg_paths:
            try:
                with winreg.OpenKey(key_type, reg_path) as key:
                    for i in range(0, winreg.QueryInfoKey(key)[0]):
                        try:
                            subkey_name = winreg.EnumKey(key, i)
                            with winreg.OpenKey(key, subkey_name) as subkey:
                                program = ProgramManager._parse_program_info(subkey, key_type, reg_path, subkey_name)
                                if program:
                                    programs.append(program)
                        except WindowsError:
                            continue
            except WindowsError:
                continue
        
        return sorted(programs, key=lambda x: x["name"].lower())

    @staticmethod
    def _parse_program_info(subkey, key_type, reg_path, subkey_name):
        try:
            display_name = winreg.QueryValueEx(subkey, "DisplayName")[0]
            if not display_name:
                return None
                
            return {
                "name": display_name,
                "uninstall": ProgramManager._get_uninstall_string(subkey),
                "install_date": ProgramManager._get_reg_value(subkey, "InstallDate"),
                "version": ProgramManager._get_reg_value(subkey, "DisplayVersion"),
                "publisher": ProgramManager._get_reg_value(subkey, "Publisher"),
                "size": ProgramManager._get_size(subkey),
                "install_location": ProgramManager._get_reg_value(subkey, "InstallLocation"),
                "reg_path": f"{key_type}:{reg_path}\\{subkey_name}"
            }
        except WindowsError:
            return None

    @staticmethod
    def _get_uninstall_string(subkey):
        try:
            return winreg.QueryValueEx(subkey, "UninstallString")[0]
        except:
            try:
                return winreg.QueryValueEx(subkey, "QuietUninstallString")[0]
            except:
                return ""

    @staticmethod
    def _get_reg_value(subkey, value_name):
        try:
            return winreg.QueryValueEx(subkey, value_name)[0]
        except:
            return ""

    @staticmethod
    def _get_size(subkey):
        try:
            size_bytes = winreg.QueryValueEx(subkey, "EstimatedSize")[0]
            return f"{size_bytes/(1024*1024):.2f} MB" if size_bytes else ""
        except:
            return ""

class Uninstaller:
    @staticmethod
    def uninstall(program_info):
        if not program_info["uninstall"]:
            return False
        
        if program_info["uninstall"].lower().startswith("msiexec"):
            return Uninstaller._uninstall_msi(program_info["uninstall"])
        
        return Uninstaller._uninstall_standard(program_info["uninstall"])

    @staticmethod
    def _uninstall_msi(uninstall_string):
        product_code = re.search(r'\{(.*?)\}', uninstall_string)
        if not product_code:
            return False
            
        command = f'msiexec /x {product_code.group(1)} /qn /norestart'
        try:
            result = subprocess.run(command, shell=True, capture_output=True, timeout=300)
            return result.returncode in (0, 3010)
        except:
            return False

    @staticmethod
    def _uninstall_standard(uninstall_path):
        uninstall_path = uninstall_path.split(" ")[0].strip('"')
        silent_params = ["/S", "/VERYSILENT", "/silent", "/qn", "-silent"]
        
        for param in silent_params:
            try:
                result = subprocess.run(
                    f'"{uninstall_path}" {param}',
                    shell=True,
                    capture_output=True,
                    timeout=300
                )
                if result.returncode in (0, 3010):
                    return True
            except:
                continue
        
        try:
            result = subprocess.run(
                f'"{uninstall_path}"',
                shell=True,
                capture_output=True,
                timeout=300
            )
            return result.returncode in (0, 3010)
        except:
            return False

class Cleaner:
    @staticmethod
    def clean(program_info):
        name = program_info["name"]
        cleaned_items = []
        
        Cleaner._clean_directories(name, program_info["install_location"], cleaned_items)
        Cleaner._clean_registry(name, cleaned_items)
        Cleaner._clean_shortcuts(name, cleaned_items)
        
        return cleaned_items

    @staticmethod
    def _clean_directories(name, install_location, cleaned_items):
        paths = [
            rf"C:\Program Files\{name}",
            rf"C:\Program Files (x86)\{name}",
            install_location,
            rf"%LOCALAPPDATA%\{name}",
            rf"%APPDATA%\{name}",
            rf"%PROGRAMDATA%\{name}"
        ]
        
        for path in paths:
            expanded_path = os.path.expandvars(path)
            if os.path.exists(expanded_path):
                try:
                    if os.path.isfile(expanded_path):
                        os.remove(expanded_path)
                        cleaned_items.append(f"文件: {expanded_path}")
                    else:
                        shutil.rmtree(expanded_path, ignore_errors=True)
                        cleaned_items.append(f"目录: {expanded_path}")
                except Exception as e:
                    cleaned_items.append(f"删除失败: {expanded_path} ({str(e)})")

    @staticmethod
    def _clean_registry(name, cleaned_items):
        reg_paths = [
            (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE"),
            (winreg.HKEY_CURRENT_USER, r"Software")
        ]
        
        for root, key_path in reg_paths:
            try:
                with winreg.OpenKey(root, key_path, 0, winreg.KEY_ALL_ACCESS) as key:
                    for i in range(winreg.QueryInfoKey(key)[0]-1, -1, -1):
                        try:
                            subkey_name = winreg.EnumKey(key, i)
                            if name.lower() in subkey_name.lower():
                                try:
                                    winreg.DeleteKey(key, subkey_name)
                                    cleaned_items.append(f"注册表: {root}\\{key_path}\\{subkey_name}")
                                except Exception as e:
                                    cleaned_items.append(f"注册表删除失败: {root}\\{key_path}\\{subkey_name} ({str(e)})")
                        except WindowsError:
                            continue
            except WindowsError:
                continue

    @staticmethod
    def _clean_shortcuts(name, cleaned_items):
        start_menu_paths = [
            rf"%APPDATA%\Microsoft\Windows\Start Menu\Programs\{name}",
            rf"%PROGRAMDATA%\Microsoft\Windows\Start Menu\Programs\{name}"
        ]
        
        for path in start_menu_paths:
            expanded_path = os.path.expandvars(path)
            if os.path.exists(expanded_path):
                try:
                    shutil.rmtree(expanded_path, ignore_errors=True)
                    cleaned_items.append(f"开始菜单: {expanded_path}")
                except Exception as e:
                    cleaned_items.append(f"开始菜单删除失败: {expanded_path} ({str(e)})")
        
        desktop_path = os.path.expandvars("%USERPROFILE%\\Desktop")
        for item in os.listdir(desktop_path):
            if name.lower() in item.lower():
                try:
                    os.remove(os.path.join(desktop_path, item))
                    cleaned_items.append(f"桌面快捷方式: {item}")
                except Exception as e:
                    cleaned_items.append(f"快捷方式删除失败: {item} ({str(e)})")

class UninstallerGUI:
    def __init__(self, root):
        self.root = root
        self.programs = []
        self.selected_program = None
        self._setup_ui()
        self._check_admin()
        self.refresh_list()

    def _check_admin(self):
        if not ProgramManager.is_admin():
            ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(sys.argv), None, 1)
            sys.exit()

    def _setup_ui(self):
        self.root.title("高级Windows软件卸载工具")
        self.root.geometry("1000x700")
        self.root.minsize(800, 600)
        
        self._create_main_frame()
        self._create_search_frame()
        self._create_program_list()
        self._create_details_panel()
        self._create_button_panel()
        self._create_status_bar()
        
        self._bind_events()

    def _create_main_frame(self):
        self.main_frame = ttk.Frame(self.root)
        self.main_frame.pack(fill=BOTH, expand=True, padx=10, pady=10)

    def _create_search_frame(self):
        search_frame = ttk.Frame(self.main_frame)
        search_frame.pack(fill=X, pady=5)
        
        ttk.Label(search_frame, text="搜索:").pack(side=LEFT)
        self.search_var = StringVar()
        search_entry = ttk.Entry(search_frame, textvariable=self.search_var, width=40)
        search_entry.pack(side=LEFT, fill=X, expand=True, padx=5)
        
        ttk.Button(search_frame, text="刷新", command=self.refresh_list).pack(side=RIGHT)

    def _create_program_list(self):
        list_frame = ttk.LabelFrame(self.main_frame, text="已安装程序")
        list_frame.pack(fill=BOTH, expand=True, pady=5)
        
        self.tree = ttk.Treeview(
            list_frame,
            columns=("name", "version", "publisher", "size", "install_date"),
            selectmode="browse"
        )
        
        columns = [
            ("name", "程序名称", 300),
            ("version", "版本", 100),
            ("publisher", "发布者", 200),
            ("size", "大小", 100),
            ("install_date", "安装日期", 120)
        ]
        
        for col_id, col_name, width in columns:
            self.tree.heading(col_id, text=col_name)
            self.tree.column(col_id, width=width)
        
        scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=self.tree.yview)
        self.tree.configure(yscrollcommand=scrollbar.set)
        scrollbar.pack(side=RIGHT, fill=Y)
        self.tree.pack(fill=BOTH, expand=True)

    def _create_details_panel(self):
        details_frame = ttk.LabelFrame(self.main_frame, text="程序详情")
        details_frame.pack(fill=X, pady=5)
        
        self.details_text = scrolledtext.ScrolledText(
            details_frame, 
            height=8, 
            state=DISABLED,
            font=("Consolas", 9)
        )
        self.details_text.pack(fill=X, padx=5, pady=5)

    def _create_button_panel(self):
        button_frame = ttk.Frame(self.main_frame)
        button_frame.pack(fill=X, pady=10)
        
        buttons = [
            ("卸载程序", self.uninstall_selected),
            ("彻底卸载并清理", self.uninstall_and_clean),
            ("导出列表", self.export_list)
        ]
        
        for text, command in buttons:
            ttk.Button(button_frame, text=text, command=command).pack(side=LEFT, padx=5)

    def _create_status_bar(self):
        self.status_var = StringVar()
        self.status_var.set("就绪")
        
        status_bar = ttk.Label(
            self.root, 
            textvariable=self.status_var, 
            relief=SUNKEN, 
            anchor=W,
            padding=5
        )
        status_bar.pack(side=BOTTOM, fill=X)

    def _bind_events(self):
        self.search_var.trace_add("write", lambda *args: self.on_search())
        self.tree.bind("<<TreeviewSelect>>", lambda event: self.on_select())

    def refresh_list(self):
        self.status_var.set("正在获取程序列表...")
        self.root.update()
        
        self.tree.delete(*self.tree.get_children())
        self.programs = ProgramManager.get_installed_programs()
        
        for idx, program in enumerate(self.programs):
            self.tree.insert(
                "", "end", 
                iid=f"program_{idx}",
                values=(
                    program["name"],
                    program["version"],
                    program["publisher"],
                    program["size"],
                    program["install_date"]
                )
            )
        
        self.status_var.set(f"就绪 | 共 {len(self.programs)} 个程序")

    def on_search(self):
        query = self.search_var.get().lower()
        
        for item in self.tree.get_children():
            values = self.tree.item(item)["values"]
            if query and query not in values[0].lower():
                self.tree.detach(item)
            else:
                self.tree.attach(item, "", "end")

    def on_select(self):
        selected = self.tree.selection()
        if not selected:
            self.selected_program = None
            self._clear_details()
            return
            
        selected_idx = int(selected[0].split("_")[1])
        self.selected_program = self.programs[selected_idx]
        self._update_details()

    def _clear_details(self):
        self.details_text.config(state=NORMAL)
        self.details_text.delete(1.0, END)
        self.details_text.config(state=DISABLED)

    def _update_details(self):
        if not self.selected_program:
            return
            
        details = [
            f"名称: {self.selected_program['name']}",
            f"版本: {self.selected_program['version']}",
            f"发布者: {self.selected_program['publisher']}",
            f"大小: {self.selected_program['size']}",
            f"安装日期: {self.selected_program['install_date']}",
            f"安装路径: {self.selected_program['install_location'] or '无'}",
            f"卸载命令: {self.selected_program['uninstall'] or '无'}"
        ]
        
        self.details_text.config(state=NORMAL)
        self.details_text.delete(1.0, END)
        self.details_text.insert(END, "\n".join(details))
        self.details_text.config(state=DISABLED)

    def uninstall_selected(self):
        if not self._validate_selection():
            return
            
        if not messagebox.askyesno("确认", f"确定卸载 {self.selected_program['name']}?"):
            return
            
        self.status_var.set(f"正在卸载 {self.selected_program['name']}...")
        self.root.update()
        
        if Uninstaller.uninstall(self.selected_program):
            messagebox.showinfo("成功", f"{self.selected_program['name']} 已卸载")
            self.refresh_list()
        else:
            messagebox.showerror("错误", "卸载失败，请尝试手动卸载")

    def uninstall_and_clean(self):
        if not self._validate_selection():
            return
            
        if not messagebox.askyesno("确认", f"确定卸载并清理 {self.selected_program['name']}?"):
            return
            
        self.status_var.set(f"正在卸载 {self.selected_program['name']}...")
        self.root.update()
        
        if not Uninstaller.uninstall(self.selected_program):
            messagebox.showerror("错误", "卸载失败，清理已取消")
            return
            
        time.sleep(2)
        self.status_var.set(f"正在清理 {self.selected_program['name']}...")
        self.root.update()
        
        cleaned = Cleaner.clean(self.selected_program)
        if cleaned:
            messagebox.showinfo("清理完成", f"已清理 {len(cleaned)} 项残留")
        else:
            messagebox.showinfo("清理完成", "未发现残留文件")
        
        self.refresh_list()

    def _validate_selection(self):
        if not self.selected_program:
            messagebox.showwarning("警告", "请先选择程序")
            return False
            
        if not self.selected_program["uninstall"]:
            messagebox.showwarning("警告", "该程序没有可用的卸载命令")
            return False
            
        return True

    def export_list(self):
        file_path = os.path.join(os.path.expanduser("~"), "installed_programs.json")
        try:
            with open(file_path, "w", encoding="utf-8") as f:
                json.dump(self.programs, f, ensure_ascii=False, indent=2)
            messagebox.showinfo("成功", f"列表已导出到 {file_path}")
        except Exception as e:
            messagebox.showerror("错误", f"导出失败: {str(e)}")

if __name__ == "__main__":
    root = Tk()
    app = UninstallerGUI(root)
    root.mainloop()
